//-----------------------------------------------------------------------------
// Filename: Authenticator.cs
//
// Description: 
// Provides authentication services using Username password combinations in an XML file.
// 
// History:
// 19 Jan 2004	Aaron Clauson	Created.
// 27 Jan 2004	Aaron Clauson	Altered so that directory path is passed in in constructor.
// 28 Jan 2004	Aaron Clauson	Added Credentials structure and incorporated it into the Authenticator
//								class.
// 02 Feb 2004	Aaron Clauson	Added GetSessionTimeout method.
// 15 Feb 2004	Aaron Clauson	Changed Credentials from struct to class.
// 11 Dec 2004	Aaron Clauson	Refactored for AuthenticatorProduct factory.
// 18 Dec 2004	Aaron Clauson	Added GetPassword and SetTimeout methods.
//
// License: Public Domain.
//-----------------------------------------------------------------------------

using System;
using System.IO;
using System.Xml;
using MaverickLite.Config;
using BlueFace.Sys;
using log4net;

namespace MaverickLite.Security
{
	/// <summary>
	/// The class that does the authentication and maintains authentication sessions. In a production or large
	/// deployment this class would talk to a real directory like AD or NDS, not a local xml file as it currently does.
	/// </summary>
	public class FileAuthenticator : AuthenticatorProduct
	{
		private static ILog logger = MavAppState.logger;

		private XmlDocument m_Directory = new XmlDocument();
		private string m_filePath = null;

		public FileAuthenticator(string filePath)
		{
			m_filePath = filePath;

			if(m_filePath != null && m_filePath.Trim() != String.Empty)
			{	
				if(File.Exists(m_filePath))
				{
					m_Directory.Load(m_filePath);
				}
				else
				{
					throw new ApplicationException("A non-existent security file was provided in the controller parameters, " + m_filePath + ".");
				}
			}
			else
			{
				throw new ApplicationException("An empty security file path was provided.");
			}
		}

		public override bool Authenticate(Credentials credentials, string ipAddress)
		{
			string sessionId = AuthenticateWebRequest(credentials, ipAddress);

			if(sessionId != null)
			{
				return true;
			}
			else
			{
				return false;
			}
		}
		
		/// <summary>
		/// Check with the directory whether a provided authentication id corresponds
		/// to an established session.
		/// </summary>
		/// <param name="authenticationId">An id possibly representing an established 
		/// authenticated session</param>
		/// <returns>True if authentication id valid, otherwise false.</returns>
		public override bool Authenticate(string authenticationId)
		{
			try
			{
				if(authenticationId == null || authenticationId.Length == 0)
				{
					return false;
				}
				
				// Audit this method.
				//logger.Info("Authenticate called for authenticationId " + authenticationId + ".");

				// Load the directory to ensure the latest version is used - very unthread safe.
				m_Directory.Load(m_filePath);

				XmlNode sessionNode = m_Directory.SelectSingleNode("/page/directory/account/authenticatedSession[id='" + authenticationId + "']");
			
				if(sessionNode != null)
				{
					DateTime sessionTimestamp = DateTime.ParseExact(sessionNode.SelectSingleNode("timestamp").InnerText, TIMESTAMP_FORMAT, null);

					if(((TimeSpan)DateTime.Now.ToUniversalTime().Subtract(sessionTimestamp)).TotalMinutes < m_sessionDuration.TotalMinutes)
					{			
						// Session is valid update timestamp.
						sessionNode.SelectSingleNode("timestamp").InnerText = DateTime.Now.ToUniversalTime().ToString(TIMESTAMP_FORMAT);
					
						//logger.Debug("Authenticator updating the timestamp for valid session id, " + authenticationId + ", to " + sessionNode.SelectSingleNode("timestamp").InnerText + ".");
		
						m_Directory.Save(m_filePath);

						return true;
					}
					else
					{
						//logger.Debug("Authenticator was called on an expired session. The session timestamp was " + sessionTimestamp.ToString(TIMESTAMP_FORMAT) + ", time now is " + DateTime.Now.ToString(TIMESTAMP_FORMAT) + ".");
						
						// Session is invalid remove session entry.
						sessionNode.ParentNode.RemoveChild(sessionNode);
						m_Directory.Save(m_filePath);

						return false;
					}
				}

				//logger.Warn("Authenticate was called with an unknown authentication session id, " + authenticationId + ".");

				return false;
			}
			catch(Exception excp)
			{
				logger.Error("Exception Authenticate (authenticationId): " + excp.Message);
				return false;
			}
		}

		public override string GetUsernameForSessionId(string authenticationSessionId)
		{
			XmlNode sessionNode = m_Directory.SelectSingleNode("/page/directory/account/authenticatedSession[id='" + authenticationSessionId + "']");
			return sessionNode.ParentNode.SelectSingleNode("username").InnerText;
		}

		/// <summary>
		/// Checks with the directory to determine the validity of a provided username
		/// and password combination.
		/// </summary>
		/// <param name="username"></param>
		/// <param name="password"></param>
		/// <returns>True if credentials valid, otherwise false.</returns>
		public override bool Authenticate(string username, string password)
		{
			try
			{
				// Audit this method.
				//logger.Info("Authenticate called for username " + username + ".");
				
				// Load the directory to ensure the latest version is used - very unthread safe.
				m_Directory.Load(m_filePath);

				XmlNode accountNode = m_Directory.SelectSingleNode("/page/directory/account[username='" + username + "']");
			
				if(accountNode != null)
				{
					if(password == accountNode.SelectSingleNode("password").InnerText)
					{
						return true;
					}
					else
					{
						return false;
					}
				}

				//logger.Warn("Authenticate was called with an unknown username " + username + ".");

				return false;
			}
			catch(Exception excp)
			{
				logger.Error("Exception Authenticate (username): " + excp.Message);
				return false;
			}
		}


		public override void CreateSession(string username, string authenticationId, string ipAddress)
		{
			try
			{
				// Remove any existing sessions.
				//RemoveSession(username);
				
				XmlNode accountNode = this.m_Directory.SelectSingleNode("/page/directory/account[username='" + username + "']");
				XmlElement sessionElement = this.m_Directory.CreateElement("authenticatedSession");
				XmlElement idElement = this.m_Directory.CreateElement("id");
				XmlElement timestampElement = this.m_Directory.CreateElement("timestamp");
				XmlElement ipAddressElement = this.m_Directory.CreateElement("ipaddress");

				idElement.InnerText = authenticationId;
				timestampElement.InnerText = DateTime.Now.ToUniversalTime().ToString(TIMESTAMP_FORMAT);
				ipAddressElement.InnerText = ipAddress;

				MavAppState.logger.Debug("Authenticator creating new session for " + username + ", timestamp " + timestampElement.InnerText + ".");

				sessionElement.AppendChild(idElement);
				sessionElement.AppendChild(timestampElement);
				sessionElement.AppendChild(ipAddressElement);

				accountNode.AppendChild(sessionElement);

				m_Directory.Save(m_filePath);
			}
			catch(Exception excp)
			{
				logger.Error("Exception CreateSession: " + excp.Message);
			}
		}

		public override void UpdateSessionContactTime(string sessionId)
		{
			try
			{
				// Load the directory to ensure the latest version is used - very unthread safe.
				m_Directory.Load(m_filePath);

				XmlNodeList sessionNodes = m_Directory.SelectNodes("/page/directory/account/authenticatedSession[id='" + sessionId + "']");

				foreach(XmlNode sessionNode in sessionNodes)
				{
					sessionNode.SelectSingleNode("timestamp").InnerText = DateTime.Now.ToUniversalTime().ToString(TIMESTAMP_FORMAT);
					m_Directory.Save(m_filePath);
				}
			}
			catch(Exception excp)
			{
				logger.Error("Exception RemoveSession: " + excp.Message);
			}
		}


		public override void RemoveSession(string session)
		{
			try
			{
				// Load the directory to ensure the latest version is used - very unthread safe.
				m_Directory.Load(m_filePath);

				XmlNodeList sessionNodes = m_Directory.SelectNodes("/page/directory/account/authenticatedSession[id='" + session + "']");

				foreach(XmlNode sessionNode in sessionNodes)
				{
					sessionNode.ParentNode.RemoveChild(sessionNode);
					m_Directory.Save(m_filePath);
				}

				// Check if the session parameter represents a username.
				XmlNodeList userSessionNodes = m_Directory.SelectNodes("/page/directory/account[username='" + session + "']/authenticatedSession");

				foreach(XmlNode userSessionNode in userSessionNodes)
				{
					userSessionNode.ParentNode.RemoveChild(userSessionNode);
					m_Directory.Save(m_filePath);
				}
			}
			catch(Exception excp)
			{
				logger.Error("Exception RemoveSession: " + excp.Message);
			}
		}

		public override void RemoveSession(Credentials requestCredentials)
		{
			RemoveSession(requestCredentials.AuthenticatedSessionId);
			RemoveSession(requestCredentials.Username);
		}

		/// <summary>
		/// Attempts to authenticate against the provided credentials.
		/// </summary>
		/// <param name="requestCredentials">Set of credentials from an incoming web request.</param>
		/// <returns>If authenticated a valid session id, otherwise null.</returns>
		public override string AuthenticateWebRequest(Credentials requestCredentials, string ipAddress)
		{
			try
			{
				//logger.Debug("AuthenticateWebRequest using file authenticator.");
				
				// Load the directory to ensure the latest version is used - very unthread safe.
				m_Directory.Load(m_filePath);

				// Of course the password would not be logged.
				//MavAppState.logger.Info("AuthenticateWebRequest: username = " + requestCredentials.Username + "\r\n"
				//	+ " password = " + requestCredentials.Password + "\r\n" 
				//	+ " authentication session id = " + requestCredentials.AuthenticatedSessionId + ".");
				
				// If an authentication id is present check with the directory
				// to see whether it is valid.
				if(requestCredentials.AuthenticatedSessionId != null)
				{
					if(Authenticate(requestCredentials.AuthenticatedSessionId))
					{
						//logger.Info("AuthenticateWebRequest: authenticated session id " + requestCredentials.AuthenticatedSessionId 
						//	+ " for user " + GetUsernameForSessionId(requestCredentials.AuthenticatedSessionId) + " at " + DateTime.Now.ToString(TIMESTAMP_FORMAT) + ".");

						return requestCredentials.AuthenticatedSessionId;
					}
				}

				// If not authenticated from state information check for valid username and password information.
				if(!Authenticate(requestCredentials.Username, requestCredentials.Password))
				{
					//logger.Warn("AuthenticateWebRequest: authentication for user " + requestCredentials.Username 
					//	+ " failed at " +  DateTime.Now.ToString(TIMESTAMP_FORMAT) + ".");
					
					return null;
				}
				else
				{
					string sessionId = Guid.NewGuid().ToString();
					
					//logger.Info("AuthenticateWebRequest: authenticated user " + requestCredentials.Username 
					//		+ " authentication session id assigned " + sessionId + " at " + DateTime.Now.ToString(TIMESTAMP_FORMAT) + ".");

					// Create a session in the directory.
					CreateSession(requestCredentials.Username, sessionId, ipAddress);

					return sessionId;
				}
			}
			catch(Exception excp)
			{
				logger.Error("Exception AuthenticateWebRequest: " + excp.Message);
				return null;
			}
		}


		public override DateTime GetSessionTimeout(string sessionId)
		{
			try
			{
				XmlNode sessionNode = m_Directory.SelectSingleNode("/page/directory/account/authenticatedSession[id = '" + sessionId + "']");

				if(sessionNode != null)
				{
					string textTimestamp = sessionNode.SelectSingleNode("timestamp").InnerText;
					DateTime timestamp = DateTime.Parse(textTimestamp).AddMinutes(m_sessionDuration.TotalMinutes);
					
					//logger.Info("GetSessionTimeout returning " + textTimestamp + " for session id " + sessionId + " ( " + GetUsernameForSessionId(sessionId) + " ).");

					return timestamp;
				}

				//logger.Warn("GetSessionTimeout was requested to get a timestamp for an invalid or expired session id, " + sessionId + " ( " + GetUsernameForSessionId(sessionId) + " ).");

				return DateTime.MinValue;
			}
			catch(Exception excp)
			{
				logger.Error("Exception GetSessionTimeout: " + excp.Message);
				return DateTime.MinValue;
			}
		}

		/// <summary>
		/// Retrieves the password for an account for the given account name or email address.
		/// </summary>
		/// <param name="account">A string parameter that represents an account name or an email address.</param>
		/*public override void GetPassword(string account, out string emailAddress, out string username, out string password)
		{
			emailAddress = null;
			username = null;
			password = null;
		}*/

		/// <summary>
		/// Sets the period that authenticated sessions will remain valid for.
		/// </summary>
		/// <param name="duration"></param>
		public override void SetTimeout(TimeSpan duration)
		{
			//logger.Debug("The FileAuthenticator timeout set to " + duration.TotalMinutes + " minutes.");

			this.m_sessionDuration = duration;
		}
	}
}
